package studio.baka.satoripixeldungeon.levels.builders;

import studio.baka.satoripixeldungeon.levels.rooms.Room;
import com.watabou.utils.*;

import java.util.ArrayList;

public abstract class Builder {

    //If builders require additional parameters, they should
    // request them in their constructor or other methods

    //builders take a list of rooms and returns them as a connected map
    //returns null on failure
    public abstract ArrayList<Room> build(ArrayList<Room> rooms);

    protected static void findNeighbours(ArrayList<Room> rooms) {
        Room[] ra = rooms.toArray(new Room[0]);
        for (int i = 0; i < ra.length - 1; i++) {
            for (int j = i + 1; j < ra.length; j++) {
                ra[i].addNeigbour(ra[j]);
            }
        }
    }

    //returns a rectangle representing the maximum amount of free space from a specific start point
    protected static Rect findFreeSpace(Point start, ArrayList<Room> collision, int maxSize) {
        Rect space = new Rect(start.x - maxSize, start.y - maxSize, start.x + maxSize, start.y + maxSize);

        //shallow copy
        ArrayList<Room> colliding = new ArrayList<>(collision);
        do {

            //remove empty rooms and any rooms we aren't currently overlapping
            //if not colliding
            colliding.removeIf(room -> room.isEmpty()
                    || Math.max(space.left, room.left) >= Math.min(space.right, room.right)
                    || Math.max(space.top, room.top) >= Math.min(space.bottom, room.bottom));

            //iterate through all rooms we are overlapping, and find the closest one
            Room closestRoom = null;
            int closestDiff = Integer.MAX_VALUE;
            boolean inside = true;
            int curDiff = 0;
            for (Room curRoom : colliding) {

                if (start.x <= curRoom.left) {
                    inside = false;
                    curDiff += curRoom.left - start.x;
                } else if (start.x >= curRoom.right) {
                    inside = false;
                    curDiff += start.x - curRoom.right;
                }

                if (start.y <= curRoom.top) {
                    inside = false;
                    curDiff += curRoom.top - start.y;
                } else if (start.y >= curRoom.bottom) {
                    inside = false;
                    curDiff += start.y - curRoom.bottom;
                }

                if (inside) {
                    space.set(start.x, start.y, start.x, start.y);
                    return space;
                }

                if (curDiff < closestDiff) {
                    closestDiff = curDiff;
                    closestRoom = curRoom;
                }

            }

            int wDiff, hDiff;
            if (closestRoom != null) {

                wDiff = Integer.MAX_VALUE;
                if (closestRoom.left >= start.x) {
                    wDiff = (space.right - closestRoom.left) * (space.height() + 1);
                } else if (closestRoom.right <= start.x) {
                    wDiff = (closestRoom.right - space.left) * (space.height() + 1);
                }

                hDiff = Integer.MAX_VALUE;
                if (closestRoom.top >= start.y) {
                    hDiff = (space.bottom - closestRoom.top) * (space.width() + 1);
                } else if (closestRoom.bottom <= start.y) {
                    hDiff = (closestRoom.bottom - space.top) * (space.width() + 1);
                }

                //reduce by as little as possible to resolve the collision
                if (wDiff < hDiff || wDiff == hDiff && Random.Int(2) == 0) {
                    if (closestRoom.left >= start.x && closestRoom.left < space.right) space.right = closestRoom.left;
                    if (closestRoom.right <= start.x && closestRoom.right > space.left) space.left = closestRoom.right;
                } else {
                    if (closestRoom.top >= start.y && closestRoom.top < space.bottom) space.bottom = closestRoom.top;
                    if (closestRoom.bottom <= start.y && closestRoom.bottom > space.top) space.top = closestRoom.bottom;
                }
                colliding.remove(closestRoom);
            } else {
                colliding.clear();
            }

            //loop until we are no longer colliding with any rooms
        } while (!colliding.isEmpty());

        return space;
    }

    private static final double A = 180 / Math.PI;

    //returns the angle in degrees made by the centerpoints of 2 rooms, with 0 being straight up.
    protected static float angleBetweenRooms(Room from, Room to) {
        PointF fromCenter = new PointF((from.left + from.right) / 2f, (from.top + from.bottom) / 2f);
        PointF toCenter = new PointF((to.left + to.right) / 2f, (to.top + to.bottom) / 2f);
        return angleBetweenPoints(fromCenter, toCenter);
    }

    protected static float angleBetweenPoints(PointF from, PointF to) {
        double m = (to.y - from.y) / (to.x - from.x);

        float angle = (float) (A * (Math.atan(m) + Math.PI / 2.0));
        if (from.x > to.x) angle -= 180f;
        return angle;
    }

    //Attempts to place a room such that the angle between the center of the previous room
    // and it matches the given angle ([0-360), where 0 is straight up) as closely as possible.
    //Note that getting an exactly correct angle is harder the closer that angle is to diagonal.
    //Returns the exact angle between the centerpoints of the two rooms, or -1 if placement fails.
    protected static float placeRoom(ArrayList<Room> collision, Room prev, Room next, float angle) {

        //wrap angle around to always be [0-360)
        angle %= 360f;
        if (angle < 0) {
            angle += 360f;
        }

        PointF prevCenter = new PointF((prev.left + prev.right) / 2f, (prev.top + prev.bottom) / 2f);

        // calculating using y = mx+b, straight line formula
        double m = Math.tan(angle / A + Math.PI / 2.0);
        double b = prevCenter.y - m * prevCenter.x;

        //using the line equation, we find the point along the prev room where the line exists
        Point start;
        int direction;
        if (Math.abs(m) >= 1) {
            if (angle < 90 || angle > 270) {
                direction = Room.TOP;
                start = new Point((int) Math.round((prev.top - b) / m), prev.top);
            } else {
                direction = Room.BOTTOM;
                start = new Point((int) Math.round((prev.bottom - b) / m), prev.bottom);
            }
        } else {
            if (angle < 180) {
                direction = Room.RIGHT;
                start = new Point(prev.right, (int) Math.round(m * prev.right + b));
            } else {
                direction = Room.LEFT;
                start = new Point(prev.left, (int) Math.round(m * prev.left + b));
            }
        }

        //cap it to a valid connection point for most rooms
        if (direction == Room.TOP || direction == Room.BOTTOM) {
            start.x = (int) GameMath.gate(prev.left + 1, start.x, prev.right - 1);
        } else {
            start.y = (int) GameMath.gate(prev.top + 1, start.y, prev.bottom - 1);
        }

        //space checking
        Rect space = findFreeSpace(start, collision, Math.max(next.maxWidth(), next.maxHeight()));
        if (!next.setSizeWithLimit(space.width() + 1, space.height() + 1)) {
            return -1;
        }

        //find the ideal center for this new room using the line equation and known dimensions
        PointF targetCenter = new PointF();
        if (direction == Room.TOP) {
            targetCenter.y = prev.top - (next.height() - 1) / 2f;
            targetCenter.x = (float) ((targetCenter.y - b) / m);
            next.setPos(Math.round(targetCenter.x - (next.width() - 1) / 2f), prev.top - (next.height() - 1));

        } else if (direction == Room.BOTTOM) {
            targetCenter.y = prev.bottom + (next.height() - 1) / 2f;
            targetCenter.x = (float) ((targetCenter.y - b) / m);
            next.setPos(Math.round(targetCenter.x - (next.width() - 1) / 2f), prev.bottom);

        } else if (direction == Room.RIGHT) {
            targetCenter.x = prev.right + (next.width() - 1) / 2f;
            targetCenter.y = (float) (m * targetCenter.x + b);
            next.setPos(prev.right, Math.round(targetCenter.y - (next.height() - 1) / 2f));

        } else {
            targetCenter.x = prev.left - (next.width() - 1) / 2f;
            targetCenter.y = (float) (m * targetCenter.x + b);
            next.setPos(prev.left - (next.width() - 1), Math.round(targetCenter.y - (next.height() - 1) / 2f));

        }

        //perform connection bounds and target checking, move the room if necessary
        if (direction == Room.TOP || direction == Room.BOTTOM) {
            if (next.right < prev.left + 2) next.shift(prev.left + 2 - next.right, 0);
            else if (next.left > prev.right - 2) next.shift(prev.right - 2 - next.left, 0);

            if (next.right > space.right) next.shift(space.right - next.right, 0);
            else if (next.left < space.left) next.shift(space.left - next.left, 0);
        } else {
            if (next.bottom < prev.top + 2) next.shift(0, prev.top + 2 - next.bottom);
            else if (next.top > prev.bottom - 2) next.shift(0, prev.bottom - 2 - next.top);

            if (next.bottom > space.bottom) next.shift(0, space.bottom - next.bottom);
            else if (next.top < space.top) next.shift(0, space.top - next.top);
        }

        //attempt to connect, return the result angle if successful.
        if (next.connect(prev)) {
            return angleBetweenRooms(prev, next);
        } else {
            return -1;
        }
    }
}
